{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 第8章 循环神经网络\n",
    "\n",
    "## 8.1 循环神经网络简介\n",
    "循环神经网络（recurrent neural network, RNN）源自1982年由Saratha Sathasivam提出的[霍普菲尔德网络](https://arxiv.org/abs/0804.4075)。霍普菲尔德网络因为实现困难，在提出时并且没有被合适地应用。该网络结构也于1986年后被全连接神经网络以及一些传统的机器学习算法所取代。然而：\n",
    "- **传统的机器学习算法**非常依赖于人工提取的特征，使得基于传统机器学习的图像识别、语音识别以及自然语言处理等问题存在特征提取的瓶颈；\n",
    "- **基于全连接神经网络**的方法也存在参数太多、无法利用数据中时间序列信息等问题。\n",
    "\n",
    "随着更加有效的循环神经网络结构被不断提出，循环神经网络挖掘数据中的时序信息以及语义信息的深度表达能力被充分利用，并在语音识别、语言模型、机器翻译以及时序分析等方面实现了突破。\n",
    "\n",
    "**1. 原理**\n",
    "\n",
    "**RNN的主要用途是处理和预测序列数据。**在之前介绍的全连接神经网络或CNN模型中，网络结构都是从输入层到隐含层再到输出层，层与层之间是全连接或部分连接的，**但每层之间的节点是无连接的**。考虑这样一个问题，如果要预测句子的下一个单词是什么，一般需要用到当前单词以及前面的单词，因为句子中前后单词并不是独立的。比如，当前单词是“很”，前一个单词是“天空”，那么下一个单词很大概率是“蓝”。RNN的来源就是为了刻画一个序列当前的输出与之前信息的关系。从网络结构上，RNN会记忆之前的信息，并利用之前的信息影响后面结点的输出。也就是说，**RNN的隐藏层之间的结点是有连接的，隐藏层的输入不仅包括输入层的输出，还包括上一时刻隐藏层的输出。**\n",
    "\n",
    "下图展示了一个典型的RNN。在每一时刻$t$，RNN会针对该时刻的输入结合当前模型的状态给出一个输出，并更新模型状态。从图8.1中可以看到，RNN的主体结构A的输入除了来自输入$x_t$，还有一个循环的边来提供上一时刻的隐藏状态（hidden state)$h_{t-1}$。在每一个时刻，RNN的模块A在读取了$x_t$和$h_{t-1}$ 之后会生成新的隐藏状态$h_t$，并产生本时刻的输出$o_t$，由于模块A中的运算和变量在不同时刻是相同的，**因此RNN络理论上可以被看作是同一神经网络结构被无限复制的结果。正如卷积神经网络在不同的空间位置共享参数，循环神经网络是在不同时间位置共享参数，从而能够使用有限的参数处理任意长度的序列。**\n",
    "<p align='center'>\n",
    "    <img src=images/图8.1.JPG>\n",
    "</p>\n",
    "\n",
    "将完整的输入输出序列展开，可以得到图8.2所展示的结构。在图8.2中可以更加清楚地看到RNN在每一个时刻会有一个输入$x_t$，然后根据RNN前一时刻的状态$h_{t-1}$计算新的状态$h_t$，并输出$o_t$。RNN当前的状态$h_t$， 是根据上一时刻的状态$h_{t-1}$和当前的输入$x_t$共同决定的。在时刻$t$，状态$h_{t-1}$浓缩了前面序列$x_0, x_1,..., x_{t-1}$的信息，用于作为输出$o_t$的参考。由于序列的长度可以无限延长，维度有限的$h$状态不可能将序列的全部信息都保存下来，因此模型必须学习只保留与后面任务$o_t, o_{t+1}, ...$相关的最重要的信息。\n",
    "\n",
    "循环网络的展开在模型训练中有重要意义。从8.2图中可以看到，RNN对长度为N的序列展开之后，可以视为一个有N个中间层的前馈神经网络。这个前馈神经网路没有循环链接，因此可以直接使用反向传播算法进行训练，而不需要任何特别的优化算法。这样的训练方法称为“沿时间反向传播” （Back-Propagation Through Time），是训练循环神经网络最常见的方法。\n",
    "<p align='center'>\n",
    "    <img src=images/图8.2.JPG>\n",
    "</p>\n",
    "\n",
    "**2. 应用**\n",
    "\n",
    "从RNN的结构特征可以很容易看出它最擅长解决与时间序列相关的问题。RNN也是处理这类问题时最自然的神经网络结构。对于一个序列数据，可以将这个序列上不同时刻的数据依次传入RNN的输入层，而输出可以是对序列中下一个时刻的预测，也可以是对当前时刻信息的处理结果（比如语音识别结果）。循环神经网络要求每一个时刻都有一个输入，但是不一定每个时刻都需要有输出。**在过去几年中，RNN已经被广泛地应用在语音识别、语言模型、机器翻译以及时序分析等问题上，并取得了巨大的成功。**\n",
    "\n",
    "以机器翻译为例来介绍RNN是如何解决实际问题的。RNN中每一个时刻的输入为需要翻译的句子中的单词。如图8.3所示，需要翻译的句子为ABCD，那么RNN第一段每一个时刻的输入就分别是A、B、C和D，然后用“\\_”作为待翻译句子的结束符。在第一段中，循环神经网络没有输出。从结束符“\\_”开始，循环神经网络进入翻译阶段。该阶段中每一个时刻的输入是上一个时刻的输出，而最终得到的输出就是句子ABCD翻译的结果。从图8.3中可以看到句子ABCD对应的翻译结果就是XYZ，当网络输出“_＂时翻译结束。机器翻译的相关模型将在第9章中进行进一步介绍。\n",
    "<p align='center'>\n",
    "    <img src=images/图8.3.JPG>\n",
    "</p>\n",
    "\n",
    "**3. RNN前向传播**\n",
    "\n",
    "如之前所介绍，RNN可以看作是同一神经网络结构在时间序列上被复制多次的结果，**这个被复制多次的结构被称之为循环体。如何设计循环体的网络结构是循环神经网络解决实际问题的关键。**图8.4展示了一个最简单的循环体结构。这个循环体中只使用了一个类似全连接层的神经网络结构。下面将通过图8.4中所展示的神经网络来介绍RNN前向传播的完整流程。\n",
    "<p align='center'>\n",
    "    <img src=images/图8.4.JPG>\n",
    "</p>\n",
    "\n",
    "RNN中的状态是通过一个向量来表示的，这个向量的维度也称为RNN隐藏层的大小，假设其为$n$。从图8.4中可以看出，循环体中的神经网络的输入有两部分，一部分为上一时刻的状态，另一部分为当前时刻的输入样本。对于时间序列数据来说（比如不同时刻商品的销量），每一时刻的输入样例可以是当前时刻的数值（比如销量值）；对于语言模但来说，输入样例可以是当前单词对应的单词向量（word embedding）。\n",
    "\n",
    "假设输入向量的维度为$x$，隐藏状态的维度为$n$，那么图8.4中循环体的全连接层神经网络的输入大小为$n+x$。也就是将上一时刻的状态与当前时刻的输入拼接成一个大的向量作为循环体中神经网络的输入。因为该全连接层的输出为当前时刻的状态，于是输出层的节点个数也为n，循环体中的参数个数为$（n+x）*n+n $个。从图8.4中可以看到，循环体中的神经网络输出不但提供给了下一时刻作为状态，同时也会提供给当前时刻的输出。**注意到循环体状态与最终输出的维度通常不同，因此为了将当前时刻的状态转化为最终的输出，RNN还需要另外一个全连接神经网络来完成这个过程。这和CNN中最后的全连接层的意义是一样的。**类似的，不同时刻用于输出的全连接神经网络中的参数也是一致的。\n",
    "\n",
    "\n",
    "**4. 前向传播例子**\n",
    "\n",
    "为了让读者对RNN的前向传播有一个更加直观的认识，图8.5展示了一个RNN前向传播的具体计算过程。\n",
    "<p align='center'>\n",
    "    <img src=images/图8.5.JPG>\n",
    "</p>\n",
    "\n",
    "在图8.5中，假设状态地维度为2，输入、输出地维度都为1，而且循环体中地全连接层中的权重为：\n",
    "$$ w_{rnn} = \n",
    "  \\left[ \\begin{matrix}\n",
    "   0.1 & 0.2 \\\\\n",
    "   0.3 & 0.4 \\\\\n",
    "   0.5 & 0.6\n",
    "  \\end{matrix} \\right]\n",
    "$$\n",
    "偏置项的大小为$b_{rnn} = \\left[0.1, -0.1 \\right]$，用于输出的全连接层权重为：\n",
    "$$ w_{output} = \n",
    "  \\left[ \\begin{matrix}\n",
    "   1.0 \\\\\n",
    "   2.0\n",
    "  \\end{matrix} \\right]\n",
    "$$\n",
    "偏置项的大小为$b_{output} = 0.1$。那么在时刻$t_0$，因为没有上一时刻，所以将状态初始化为$h_{init} = \\left[0, 0 \\right]$，而当前的输入为1，所以拼接得到的向量为$\\left[ 0, 0, 1 \\right]$，通过循环体中的全连接层神经网络得到的结果为：\n",
    "$$tanh\\left( [0, 0, 1] * \\left[ \\begin{matrix} 0.1 & 0.2 \\\\ 0.3 & 0.4 \\\\0.5 & 0.6 \\end{matrix} \\right] + [0.1, -0.1] \\right) =  tanh([0.6, 0.5]) = [0.537, 0.462]$$\n",
    "这个结果将作为下一时刻的输入状态，同时RNN也会使用该状态生成输出。将该向盘作为输入提供给用于输出的全连接神经网络可以得到$t_0$时刻的最终输出：\n",
    "$$[0.537, 0.462] * \\left[ \\begin{matrix} 1.0 \\\\ 2.0 \\end{matrix} \\right] + 0.1 = 1.56$$\n",
    "使用$t_0$时刻的状态可以类似地推导得出$t_1$时刻的状态为[0.860, 0.884]，而$t_1$时刻的输出为2.73。**在得到RNN的前向传播结果之后，可以和其他神经网络类似地定义损失函数。RNN唯一的区别在于因为它每个时刻都有一个输出，所以RNN的总损失为所有时刻（或者部分时刻）上的损失函数的总和。**以下代码实现了这个简单的循环神经网络前向传播的过程："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "before activation:  [0.6 0.5]\n",
      "state:  [0.53704957 0.46211716]\n",
      "output:  [1.56128388]\n",
      "before activation:  [1.2923401  1.39225678]\n",
      "state:  [0.85973818 0.88366641]\n",
      "output:  [2.72707101]\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "X = [1,2]\n",
    "state = [0.0, 0.0]\n",
    "# 分开定义不同输入部分的权重以方便操作\n",
    "w_cell_state = np.asarray([[0.1, 0.2], [0.3, 0.4]])\n",
    "w_cell_input = np.asarray([0.5, 0.6])\n",
    "b_cell = np.asarray([0.1, -0.1])\n",
    "\n",
    "# 定义用于输出的全连接层参数\n",
    "w_output = np.asarray([[1.0], [2.0]])\n",
    "b_output = 0.1\n",
    "\n",
    "# 执行前向传播过程\n",
    "for i in range(len(X)):\n",
    "    before_activation = np.dot(state, w_cell_state) + X[i] * w_cell_input + b_cell\n",
    "    state = np.tanh(before_activation)\n",
    "    final_output = np.dot(state, w_output) + b_output\n",
    "    print(\"before activation: \", before_activation)\n",
    "    print(\"state: \", state)\n",
    "    print(\"output: \", final_output)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "和其他神经网络类似，在定义完损失函数之后，套用第4章中介绍的优化框架TensorFlow就可以自动完成模型训练的过程。这里唯一需要特别指出的是，理论上RNN可以支持任意长度的序列，然而在实际训练过程中，如果序列过长:\n",
    "- 一方面会导致优化时出现**梯度消散和梯度爆炸**的问题;\n",
    "- 另一方面，展开后的前馈神经网络会**占用过大的内存**。\n",
    "\n",
    "所以实际中一般会规定一个最大长度，当序列长度超过规定长度之后会对序列进行截断。\n",
    "\n",
    "## 8.2 长短时记忆网络（LSTM）结构\n",
    "**1. 背景**\n",
    "\n",
    "RNN通过保存历史信息来帮助当前的决策，例如使用之前出现的单词来加强对当前文字的理解。RNN可以更好地利用传统神经网络结构所不能建模的信息，但同时，这也带来了更大的技术挑战一一**长期依赖（long-term dependencies）问题**。在有些问题中，模型仅仅需要短期内的信息来执行当前的任务。比如预测短语“大海的颜色是蓝色”中的最后一个单词“蓝色”时，模型并不需要记忆这个短语之前更长的上下文信息一一因为这一句话已经包含了足够的信息来预测最后一个词。在这样的场景中，相关的信息和待预测的词的位置之间的间隔很小，RNN可以比较容易地利用先前信息。\n",
    "\n",
    "但同样也会有一些上下文场景更加复杂的情况。比如当模型试着去预测段落“某地开设了大量工厂， 空气污染十分严重……这里的天空都是灰色的”的最后一个单词时，仅仅根据短期依赖就无法很好的解决这种问题。因为只根据最后一小段，最后一个词可以是“蓝色的”或者“灰色的”。但如果模型需要预测清楚具体是什么颜色，就需要考虑先前提到但离当前位置较远的上下文信息。因此，当前预测位置和相关信息之间的文本间隔就有可能变得很大。当这个问隔不断增大时，类似图8.4中给出的简单RNN有可能会丧失学习到距离如此远的信息的能力。或者在复杂语言场景中，有用信息的间隔有大有小、长短不一，循环神经网络的性能也会受到限制。\n",
    "\n",
    "**2. 原理：**\n",
    "长短时记忆网络（long short-term memory，LSTM）的设计就是为了解决这个问题。在很多的任务上，采用LSTM结构的RNN比标准的RNN表现更好。LSTM结构是由Sepp Hochreiter和Jurgen Schmidhuber于1997年提出的，它是一种特殊的循环体结构。如图8.6所示，与单一tanh循环体结构不同，LSTM是一种拥有三个“门”结构的特殊网络结构。\n",
    "<p align='center'>\n",
    "    <img src=images/图8.6.JPG>\n",
    "</p>\n",
    "\n",
    "**\"门结构\"：LSTM 靠一些“门”的结构让信息有选择性地影响循环神经网络中每个时刻的状态。所谓“门”的结构就是一个使用sigmoid神经网络和一个按位做乘法的操作，这两个操作合在一起就是一个“门”的结构**。之所以该结构叫做“门”是因为使用sigmoid作为激活函数的全连接神经网络层会输出一个0到1之间的数值，描述当前输入有多少信息量可以通过这个结构。于是这个结构的功能就类似于一扇门， 当门打开时（sigmoid神经网络层输出为1时），全部信息都可以通过；当门关上时（sigmoid神经网络层输出为0时），任何信息都无法通过。本节下面的篇幅将介绍每一个“门”是如何工作的。\n",
    "\n",
    "为了使循环神经网更有效的保存长期记忆，图8.6中“遗忘门＂和“输入门”至关重要，它们是LSTM结构的核心：\n",
    "- **“遗忘门”的作用是让循环神经网络“忘记”之前没有用的信息。**比如一段文章中先介绍了某地原来是绿水蓝天，但后来被污染了。于是在看到被污染了之后，循环神经网络应该“忘记”之前绿水蓝天的状态。这个工作是通过“遗忘门”来完成的。**“遗忘门”会根据当前的输入$x_t$和上一时刻输出$h_{t-1}$决定哪一部分记忆需要被遗忘。**假设状态c的维度为n。“遗忘门”会根据当前的输入$x_t$和上一时刻输出$h_{t-1}$计算一个维度为n的向量$f = sigmoid(W_1x + W_2h)$，它在每一维度上的值都在(0, 1)范围内。再将上一时刻的状态$c_{t-1}$与$f$向量按位相乘，那么$f$取值接近0的维度上的信息就会被“忘记”，而$f$取值接近1的维度上的信息会被保留。\n",
    "- **在RNN“忘记”了部分之前的状态后，它还需要从当前的输入补充最新的记忆，这个过程就是“输入门”完成的。**如图8.6所示，**“输入门＂会根据$x_t$ 和$h_{t-1}$决定哪些信息加入到状态$c_{t-1}$中生成新的状态$c_t$。**比如当看到文章中提到环境被污染之后，模型需要将这个信息写入新的状态。这时“输入门”和需要写入的新状态都从$x_t$和$h_{t-1}$计算产生。通过“遗忘门”和“输入门”，LSTM结构可以更加有效地决定哪些信息应该被遗忘，哪些信息应该得到保留。\n",
    "\n",
    "**“输出门”：LSTM结构在计算得到新的状态$c_t$后需要产生当前时刻的输出，这个过程是通过“输出门”完成的。“输出门”会根据最新的状态$c_t$、上一时刻的输出$h_{t-1}$和当前的输入$x_t$来决定该时刻的输出值。**比如当前的状态为被污染，那么“天空的颜色”后面的单词很可能就是“灰色的”。相比图8.4中展示的RNN，使用LSTM结构的循环神经网络的前向传播是一个相对比较复杂的过程。具体LSTM每个“门”的公式定义如下：\n",
    "$$z = tanh(W_z[H_{t-1}, x_t]) \\tag{输入值}$$\n",
    "$$i = sigmoid(W_i[H_{t-1}, x_t]) \\tag{输入门}$$\n",
    "$$f = sigmoid(W_f[H_{t-1}, x_t]) \\tag{遗忘门}$$\n",
    "$$o = sigmoid(W_o[H_{t-1}, x_t]) \\tag{输出门}$$\n",
    "$$c_t = f * c_{t-1} + i * z \\tag{新状态}$$\n",
    "$$h_t = o * tanh(c_t) \\tag{输出}$$\n",
    "\n",
    "其中$W_z、W_i、W_f、W_o$是4个维度为[2n, n]的参数矩阵。下图用流程图的形式表示了上面的公式：\n",
    "<p align='center'>\n",
    "    <img src=images/图8.7.JPG>\n",
    "</p>\n",
    "\n",
    "在TensorFlow中，LSTM结构可以被很简单地实现。以下代码展示了在TensorFlow中实现使用LSTM结构的循环神经网络的前向传播过程："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "# 定义一个LSTM结构。在TensorFlow中通过一句简单的命令就可以实现一个完整LSTM结构。\n",
    "# LSTM中使用的变量也会在该函数中自动被声明。\n",
    "lstm = tf.nn.rnn_cell.BasicLSTMCell(lstm hidden size)\n",
    "\n",
    "# 将LSTM中的状态初始化为全0数组。BasicLSTMCell类提供了zero_state函数来生成\n",
    "# 全零的初始状态。state是一个包含两个张量的LSTMStateTuple类，其中state.c和\n",
    "# state.h分别对应了图8.7中的c状态和h状态。\n",
    "# 和其他神经网络类似，在优化循环神经网络时，每次也会使用一个batch的训练样本。\n",
    "# 以下代码中，batch_size给出了一个batch的大小。\n",
    "state = lstm.zero_state(batch_size, tf.float32)\n",
    "\n",
    "# 定义损失函数\n",
    "loss = 0.0\n",
    "# 虽然在测试时RNN可以处理任意长度的序列，但是在训练中为了将循环网络展开成\n",
    "# 前馈神经网络，我们需要知道训练数据的序列长度。在以下代码中，用num_steps来表示\n",
    "# 这个长度。第9章将中介绍使用dynamic_rnn动态处理变长序列的方法。\n",
    "for i in range(num_steps):\n",
    "    # 在第一个时刻声明LSTM结构中使用的变量，在之后的时刻都需要复用之前定义好的变量。\n",
    "    if i > 0:tf.get_variable_scope().reuse_variables()\n",
    "        \n",
    "    # 每一步处理时间序列中的一个时刻。将当前输入current_input（图8.7的xt)\n",
    "    # 和前一时刻状态state(ht-l和ct-1）传入定义的LSTM结构可以得到当前LSTM\n",
    "    # 的输出lstm_output (ht）和更新后状态state (ht和ct）。lstm_output用于输出给\n",
    "    # 其他层，state用于输出给下一时刻，它们在dropout等方面可以有不同的处理方式。\n",
    "    lstm_output, state = lstm(current_input, state)\n",
    "    # 将当前时刻LSTM结构的输出传入一个全连接层得到最后的输出。\n",
    "    final_output = fully_connected(lstm_output)\n",
    "    # 计算当前时刻输出的损失。\n",
    "    loss += calc_loss(final_output, expected_output)\n",
    "    \n",
    "# 使用类似第4章中介绍的方法训练模型。\n",
    "'''"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通过上面这段代码看出，通过TensorFlow可以非常方便地实现使用LSTM结构的循环神经网络，而且并不需要用户对LSTM内部结构有深入的了解。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
